package com.tomtom.espresso.test; import static android.support.test.espresso.Espresso.onData; import static android.support.test.espresso.Espresso.onView; import static android.support.test.espresso.Espresso.pressBack; import static android.support.test.espresso.action.ViewActions.click; import static android.support.test.espresso.assertion.ViewAssertions.doesNotExist; import static android.support.test.espresso.assertion.ViewAssertions.matches; import static android.support.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed; import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; import static android.support.test.espresso.matcher.ViewMatchers.isEnabled; import static android.support.test.espresso.matcher.ViewMatchers.withId; import static android.support.test.espresso.matcher.ViewMatchers.withText; import static org.hamcrest.Matchers.equalToIgnoringCase; import static org.hamcrest.Matchers.hasToString; import static org.hamcrest.Matchers.not; import com.tomtom.espresso.test.actions.SpoonScreenshotAction; import cucumber.api.Scenario; import cucumber.api.java.Before; import cucumber.api.java.en.Given; import android.support.test.espresso.EspressoException; import android.support.test.espresso.NoMatchingViewException; import java.io.File; import java.io.FileInputStream; import java.io.IOException; /** * Class containing generic Cucumber test step definitions using Espresso test Instrumentation */ public class CucumberHelperTestSteps { public static final int RETRY_WAIT = 500; private static Scenario scenario; @Before public static void before(final Scenario scenario) { CucumberHelperTestSteps.scenario = scenario; } public CucumberHelperTestSteps() { } public static class ScreenshotException extends RuntimeException implements EspressoException { private static final long serialVersionUID = -1247022787790657324L; ScreenshotException(final String message) { super(message); } } public static Scenario getScenario() { return CucumberHelperTestSteps.scenario; } /** * Take a screenshot when the test scenario has failed */ public static void takeScreenshotOnFail() { if ((scenario != null) && (scenario.isFailed())) { takeScreenshot("failed"); } } /** * Take a screenshot of the current activity and embed it in the HTML report * @param tag Name of the screenshot to include in the file name */ public static void takeScreenshot(final String tag) { if (scenario == null) { throw new ScreenshotException("Error taking screenshot: I'm missing a valid test scenario to attach the screenshot to"); } SpoonScreenshotAction.perform(tag); final File screenshot = SpoonScreenshotAction.getLastScreenshot(); if (screenshot == null) { throw new ScreenshotException("Screenshot was not taken correctly, check for failures in screenshot library"); } FileInputStream screenshotStream = null; try { screenshotStream = new FileInputStream(screenshot); final byte fileContent[] = new byte[(int)screenshot.length()]; screenshotStream.read(fileContent); // Read data from input image file into an array of bytes scenario.embed(fileContent, "image/png"); // Embed the screenshot in the report under current test step } catch (final IOException ioe) { throw new ScreenshotException("Exception while reading file " + ioe); } finally { try { // close the streams using close method if (screenshotStream != null) { screenshotStream.close(); } } catch (final IOException ioe) { throw new ScreenshotException("Error while closing stream: " + ioe); } } } @Given("^I take a screenshot$") public void i_take_a_screenshot() { takeScreenshot("screenshot"); } /** * Try to press a button with some text or load button if it's in adapter * This doesn't guarantee the button is visible to the user, for example: * user needs to scroll down to press button * * @param buttonText * Text of the button to press */ public static void pressButtonWithTextOnce(final String buttonText) { try { onView(withText(buttonText)).perform(click()); } catch (final junit.framework.AssertionFailedError e) { // When item to click has to be asynchronously loaded in UI from adapter onData(hasToString(equalToIgnoringCase(buttonText))).perform(click()); } catch (final NoMatchingViewException e) { // When item to click has to be asynchronously loaded in UI from adapter onData(hasToString(equalToIgnoringCase(buttonText))).perform(click()); } catch (final RuntimeException e) { // When item to click has to be asynchronously loaded in UI from adapter onData(hasToString(equalToIgnoringCase(buttonText))).perform(click()); } } /** * Try to press a button with some text and retry for 10 seconds in case it's * asynchronously loaded This doesn't guarantee the button is visible * to the user, for example: user needs to scroll down to press button * * @param buttonText * Text of the button to press */ public static void pressButtonWithText(final String buttonText) { int retries = 20; do { try { i_see_button_enabled(buttonText, "enabled"); // Only click on enabled buttons pressButtonWithTextOnce(buttonText); return; } catch (final junit.framework.AssertionFailedError e) { try { // Retry every half a second during 10 seconds Thread.sleep(RETRY_WAIT); } catch (final InterruptedException ex) { } } catch (final NoMatchingViewException e) { try { // Retry every half a second during 10 seconds Thread.sleep(RETRY_WAIT); } catch (final InterruptedException ex) { } } catch (final RuntimeException e) { try { // Retry every half a second during 10 seconds Thread.sleep(RETRY_WAIT); } catch (final InterruptedException ex) { } } } while (--retries > 0); i_see_button_enabled(buttonText, "enabled"); // Only click on enabled buttons pressButtonWithTextOnce(buttonText); } /** * Test if some view is displayed and retry for 10 seconds in case it's asynchronously loaded * * @param id * ID of the view item to test */ public static void checkViewWithIdIsCompletelyDisplayed(final int id) { int retries = 20; do { try { onView(withId(id)).check(matches(isCompletelyDisplayed())); return; } catch (final junit.framework.AssertionFailedError e) { try { // Retry every half a second during 10 seconds Thread.sleep(RETRY_WAIT); } catch (final InterruptedException ex) { } } } while (--retries > 0); onView(withId(id)).check(matches(isCompletelyDisplayed())); } /** * Test if a view with some text is displayed or try to load it from adapter * * @param text * Text contained in the view to test */ public static void checkViewWithTextIsCompletelyDisplayedOnce(final String text) { try { onView(withText(text)).check(matches(isCompletelyDisplayed())); } catch (final junit.framework.AssertionFailedError e) { // When item to check has to be asynchronously loaded in UI from adapter onData(hasToString(equalToIgnoringCase(text))).check(matches(isCompletelyDisplayed())); } catch (final NoMatchingViewException e) { // When item to check has to be asynchronously loaded in UI from adapter onData(hasToString(equalToIgnoringCase(text))).check(matches(isCompletelyDisplayed())); } catch (final RuntimeException e) { // When item to check has to be asynchronously loaded in UI from adapter onData(hasToString(equalToIgnoringCase(text))).check(matches(isCompletelyDisplayed())); } } /** * Test if some view is displayed and retry for 10 seconds in case it's asynchronously loaded * * @param text * Text contained in the view to test */ public static void checkViewWithTextIsCompletelyDisplayed(final String text) { int retries = 20; do { try { checkViewWithTextIsCompletelyDisplayedOnce(text); return; } catch (final junit.framework.AssertionFailedError e) { try { // Retry every half a second during 10 seconds Thread.sleep(RETRY_WAIT); } catch (final InterruptedException ex) { } } catch (final NoMatchingViewException e) { try { // Retry every half a second during 10 seconds Thread.sleep(RETRY_WAIT); } catch (final InterruptedException ex) { } } catch (final RuntimeException e) { try { // Retry every half a second during 10 seconds Thread.sleep(RETRY_WAIT); } catch (final InterruptedException ex) { } } } while (--retries > 0); checkViewWithTextIsCompletelyDisplayedOnce(text); } @Given("^I press \"(.+)\"$") public static void i_press_buttonText(final String buttonText) { pressButtonWithText(buttonText); } @Given("^I press \"(.+)\" ([0-9]+) times$") public static void i_press_buttonText(final String buttonText, final int repetitions) { for (int i = 0; i < repetitions; i++) { pressButtonWithText(buttonText); } } @Given("^I see text \"(.+)\"$") public static void i_see_text(final String text) { checkViewWithTextIsCompletelyDisplayed(text); } @Given("^I don't see text \"(.+)\"$") public static void i_do_not_see_text(final String text) { onView(withText(text)).check(matches(not(isDisplayed()))); } @Given("^text \"(.+)\" does(?: not|n't) exist$") public static void text_does_not_exist(final String text) { onView(withText(text)).check(doesNotExist()); } @Given("^I press the back button$") public static void i_press_back() { pressBack(); } @Given("^I press the back button ([0-9]+) times$") public static void i_press_back(final int repetitions) { for (int i = 0; i < repetitions; i++) { i_press_back(); } } @Given("^I see \"(.+)\" button (enabled|disabled)$") public static void i_see_button_enabled(final String buttonText, final String enabled) { if ("enabled".equalsIgnoreCase(enabled)) { onView(withText(buttonText)).check(matches(isEnabled())); } else { onView(withText(buttonText)).check(matches(not(isEnabled()))); } } @Given("^I wait for ([0-9]+) seconds?$") public static void i_wait_for_seconds(final int seconds) { try { Thread.sleep(seconds * 1000); } catch (final InterruptedException e) { } } }